#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <time.h>

#include <GL/glew.h>
#include <GL/glut.h>

#include <cuda.h>
#include <cuda_runtime.h>

/* #include <cutil.h> */
/* #include <cutil_gl_error.h> */
/* #include <cuda_gl_interop.h> */

/* #include <cuda_runtime.h> */
#include <cutil_inline.h>
#include <cutil_gl_inline.h>
/* #include <cutil_gl_error.h> */
#include <cuda_gl_interop.h>
#include <vector_types.h>
#include <rendercheck_gl.h>


#include "boids.h"

#define BOID_MAX_X    (APP_WIDTH / 2)
#define BOID_MAX_Y    (APP_HEIGHT / 2)
#define BOID_STARTX   100
#define BOID_STARTY   100
#define N_MAPS        2 //5
/* static GLfloat viewerDistance = 3.0; */

char mapData[N_MAPS][80000000];

/* char map1Data[80000000]; */
/* char map2Data[80000000]; */

static GLfloat scale = 1.0;
static GLfloat viewStartx = -(MAPW / 16);
static GLfloat viewStarty = -(MAPH / 16);
static GLfloat viewWidth  = MAPW / 2;
static GLfloat viewHeight = MAPH / 2;

static GLint texNum = 0;
static GLint mapID  = 0;

unsigned int timer = 0;

// Auto-Verification Code
const int frameCheckNumber = 4;
int fpsCount = 0;        // FPS count for averaging
int fpsLimit = 1;        // FPS limit for sampling
int g_Index = 0;
unsigned int frameCount = 0;
unsigned int g_TotalErrors = 0;
bool g_Verify = false;
bool g_bQAReadback = false;
bool g_bGLVerify   = false;

// CheckFBO/BackBuffer class objects
CheckRender       *g_CheckRender = NULL;

#define MAX(a,b) ((a > b) ? a : b)

char mapList[N_MAPS][64] = { "map-00.raw",
		             "map-01.raw",
			     /* "map-extra-neon.raw", */
			     /* "map-extra-edge.raw", */
			     /* "map-extra-gaussian.raw" */
			   };

GLuint mapBits[MAPW * MAPH];


enum GlutWheel {
	WHEEL_UP = 3,
	WHEEL_DOWN
};

void init(void)
{
	glewInit();

	// default initialization
	glClearColor( 0.0, 0.0, 0.0, 1.0);
	glDisable( GL_DEPTH_TEST);

	// viewport
	glViewport( 0, 0, APP_WIDTH, APP_HEIGHT);

	// projection
	glMatrixMode( GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(60.0, (GLfloat)APP_WIDTH / (GLfloat) APP_HEIGHT, 0.1, 40.0);

	/* CUT_CHECK_ERROR_GL(); */
	cutilCheckError( cutCreateTimer( &timer));

}

void AutoQATest()
{
	if (g_CheckRender && g_CheckRender->IsQAReadback()) {
		char temp[256];
		sprintf(temp, "AutoTest: Cuda GL Interop (VBO)");
		glutSetWindowTitle(temp);
		exit(0);
	}
}

void computeFPS()
{
	frameCount++;
	fpsCount++;
	if (fpsCount == fpsLimit-1) {
		g_Verify = true;
	}
	if (fpsCount == fpsLimit) {
		char fps[256];
		float ifps = 1.f / (cutGetAverageTimerValue(timer) / 1000.f);
		sprintf(fps, "%sTaste of Chicago (VBO): %3.1f fps", 
			((g_CheckRender && g_CheckRender->IsQAReadback()) ? 
			 "AutoTest: " : ""), ifps);  

		glutSetWindowTitle(fps);
		fpsCount = 0; 
		if (g_CheckRender && 
		    !g_CheckRender->IsQAReadback()) fpsLimit = 
					(int)MAX(ifps, 1.f);

		cutilCheckError(cutResetTimer(timer));

		AutoQATest();
	}
}

void idleUpdate(void)
{
	makeBoidsUpdate();
}

void setupBack()
{

}

void drawBk(void){

}

float lpos[] = {1.0, 0.5, 1.0, 0.1};
static unsigned int mapBitsSet = 0;

void loadMapBits(void)
{
	if (mapBitsSet) return;
	mapBitsSet = 1;

	int i, j;
	GLfloat c[4];

	for (i = 0; i < MAPH; i++) {
		for(j = 0; j < MAPW; j++) {
			glReadPixels(i, j, 1, 1, GL_RGBA, GL_FLOAT, c);
			if (c[0] < 0.4 && c[1] < 0.4 && c[2] < 0.4) 
				mapBits[i * MAPW + j] = 1;
			else
				mapBits[i * MAPW + j] = 0;
		}
	}

	mapID++;
}

void display(void)
{
	cutilCheckError(cutStartTimer(timer));  

	glViewport(viewStartx, viewStarty, viewWidth, viewHeight);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glLoadIdentity();

	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, mapID);

	glColor3f(1.0, 1.0, 1.0);

	glBegin(GL_QUADS);
	glTexCoord2f(0.0f, 1.0f);
	glVertex3f(-scale, -scale, 0.0f);

	glTexCoord2f(0.0f, 0.0f);
	glVertex3f(-scale, scale, 0.0f);

	glTexCoord2f(1.0f, 0.0f);
	glVertex3f(scale, scale, 0.0f);

	glTexCoord2f(1.0f, 1.0f);
	glVertex3f(scale, -scale, 0.0f);
	glEnd();
 
	loadMapBits();

	glClear(GL_DEPTH_BUFFER_BIT);

	drawAllBoids();

	glutSwapBuffers();
	glutPostRedisplay();

	initBoids();

	cutilCheckError(cutStopTimer(timer));  
	computeFPS();
}

static inline void zoomIn(void)
{
	scale += 0.1;
	viewStartx -= (APP_WIDTH / 11);
	viewStarty -= (APP_HEIGHT / 11);
	viewWidth  *= 1.1;
	viewHeight *= 1.1;
}

static inline void zoomOut(void)
{
	if (viewWidth > APP_WIDTH 
	    && viewHeight > APP_HEIGHT) {
		scale -= 0.1;
		viewStartx += (APP_WIDTH / 10);
		viewStarty += (APP_HEIGHT / 10);
		viewWidth /= 1.1;
		viewHeight /= 1.1;
	}
}

int pMouseX, pMouseY;
GLfloat c[4];

void mouseHandler(int button, int state, int x, int y)
{
	/* printf("%d, %d\n", x, y); */

	if(state == GLUT_UP) {
		/* glGetTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, c); */
		/* glGetTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, c); */

		/* GLint viewport[4]; */
		/* GLdouble modelview[16]; */
		/* GLdouble projection[16]; */
		/* GLfloat winX, winY, winZ; */
		/* GLdouble posX, posY, posZ; */

		/* glGetDoublev(GL_MODELVIEW_MATRIX, modelview); */
		/* glGetDoublev(GL_PROJECTION_MATRIX, projection); */
		/* glGetIntegerv(GL_VIEWPORT, viewport); */

		/* winX = (float)x; */
		/* winY = (float)y; */
	
		/* glReadPixels(x, y, 1, 1, GL_RGBA, GL_FLOAT, c); */
		/* gluUnProject(winX, winY, winZ, modelview, projection, viewport, &posX, &posY, &posZ); */

		/* printf("OGL Position: %f, %f, %f  / %f, %f, %f, %f\n", posX, posY, posZ, c[0], c[1], c[2], c[3]); */


		if (button == WHEEL_UP)
			zoomIn();
		else if (button == WHEEL_DOWN)
			zoomOut();
	}
}

void passiveMouseMotion(int x, int y)
{
	pMouseY = y;
	pMouseX = x;
}

void mouseMotion(int x, int y)
{
	viewStartx += (x - pMouseX);
	viewStarty += -(y - pMouseY);

	pMouseY = y;
	pMouseX = x;
}

int setupTexture(char *buffer) 
{
	glBindTexture(GL_TEXTURE_2D, texNum);

	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, MAPW, MAPH, 0, GL_RGB, 
		     GL_UNSIGNED_BYTE, (const GLvoid *) buffer);


	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

	texNum++;

	return (texNum - 1);
}

void doTexStuff(const char *path,
		const char *fName,
		int width, int height,
		char *buffer){
	char fPath[256];
	FILE *im;

	strcpy(fPath, path);
	strcat(fPath, fName);
	im = fopen(fPath, "r");

	if(im == NULL) {
		perror(path);
		exit(1);
	}

	/* buffer = malloc(sizeof(char) * height * width * 3); */
	fread(buffer, sizeof(char), height * width * 3, im);

	fclose(im);
}

void keyboard(unsigned char key, int x, int y)
{
	switch(key) {
	case 27:
		exit(0);
		break;
	case 'm':
		mapID = (++mapID) % N_MAPS;
		break;

	case 'y':
		if(scale > 1.0) 
			zoomOut();
		break;

	case 'z':
		if(scale < 10) 
			zoomIn();
		break;

	default:
		break;
	}
}

#define R 1
#define G 2
#define B 3
#define W 4

void processMenuEvents(int option)
{

}

void createGlutMenus(void)
{
	int menu;

	menu = glutCreateMenu(processMenuEvents);

	glutAddMenuEntry("Red", R);
	glutAddMenuEntry("Green", G);
	glutAddMenuEntry("Blue", B);

	glutAttachMenu(GLUT_RIGHT_BUTTON);
}

int main(int argc, char **argv)
{

	if (argc > 1) {
		if (cutCheckCmdLineFlag(argc, (const char **)argv, "qatest") ||
		    cutCheckCmdLineFlag(argc, (const char **)argv, "noprompt")) 
		{
			printf("- (automated test no-OpenGL)\n");
			g_bQAReadback = true;
//			g_bGLVerify = true;	
			fpsLimit = frameCheckNumber;
		} else if (cutCheckCmdLineFlag(argc, (const char **)argv, "glverify")) {
			printf("- (automated test OpenGL rendering)\n");
			g_bGLVerify = true;	
			fpsLimit = frameCheckNumber;
		}
	}

	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
	glutInitWindowSize(APP_WIDTH, APP_HEIGHT);
	glutInitWindowPosition(BOID_STARTX, BOID_STARTY);
	glutCreateWindow("Taste of Chicago");

	init();

	glutKeyboardFunc(keyboard);
	glutMouseFunc(mouseHandler);
	glutPassiveMotionFunc(passiveMouseMotion);
	glutMotionFunc(mouseMotion);
	glutDisplayFunc(display);
	glutIdleFunc(idleUpdate);

	glEnable(GL_DEPTH_TEST);
	glClearColor(0.0, 0.0, 0.0, 1.0);

	glBindTexture(GL_TEXTURE_2D, 0);

	for(int i = 0; i < N_MAPS; i++) {
		doTexStuff((char *) "./images/", (char *) mapList[i], 
			   MAPW, MAPH, mapData[i]);
		setupTexture(mapData[i]);
	}

	createGlutMenus();

	initOther();
	glutMainLoop();

	return 0;
}
